package com.mvu.banana.tools;

import com.sun.xml.xsom.*;
import com.sun.xml.xsom.parser.XSOMParser;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @goal codegen
 */
public class EntityGenerator extends AbstractMojo {
  /**
   * The directory to look for input files (xsds).
   *
   * @parameter expression="${schemaDirectory}" default-value="src/main/domain"
   */
  private String inputFolder;
  /**
   * The directory to output code.
   *
   * @parameter expression="${outputDirectory}" default-value="src/main/java"
   */
  private String outputFolder;

  private FilenameFilter filter = new FilenameFilter() {
    public boolean accept(File file, String s) {
      return s.endsWith(".xsd") && !s.equals("datatypes.xsd");
    }
  };

  public void execute() throws MojoExecutionException {
    getLog().info("Execute...\t\t" + this.getClass().getName());
    File domainDir = new File(inputFolder);
    getLog().info("Working dir...\t" + domainDir.getAbsolutePath());
    File outputDir = new File(outputFolder);
    getLog().info("Output dir...\t" + outputDir.getAbsolutePath());
    for (File xsd : domainDir.listFiles(filter)) {
      getLog().info("Processing...\t" + xsd.getName());
      XSOMParser parser = new XSOMParser();
      try {
        parser.parse(xsd);
        XSSchemaSet schemaSet = parser.getResult();
        XSSchema schema = schemaSet.getSchema(1);
        String targetNamespace = schema.getTargetNamespace();
        getLog().info("Schema found...\t" + targetNamespace);
        for (Map.Entry<String, XSSimpleType> entry : schema.getSimpleTypes().entrySet()) {
          generateEnum(entry, targetNamespace);
        }
        for (Map.Entry<String, XSComplexType> entry : schema.getComplexTypes().entrySet()) {
          generateType(entry, targetNamespace);
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

  private void generateType(Map.Entry<String, XSComplexType> entry, String targetNamespace) {
    XSComplexType type = entry.getValue();
    getLog().info("Type found...\t" + type.getName());
    JavaClass javaClass = new JavaClass().withName(targetNamespace + ".stub", type.getName())
            .withSuperClass("com.mvu.banana.domain.impl.KeyableBean")
            .withAnnotation(Entity.class);
    XSContentType xsContentType = type.getContentType();
    XSParticle particle = xsContentType.asParticle();
    XSTerm term = particle.getTerm();
    XSModelGroup xsModelGroup = term.asModelGroup();
    XSParticle[] children = xsModelGroup.getChildren();
    if (!children[0].getTerm().isElementDecl()) {
      // extension
      XSParticle[] base = children[0].asParticle().getTerm().asModelGroup().getChildren();
      XSParticle[] extension = children[1].asParticle().getTerm().asModelGroup().getChildren();
      children = combineArray(base, extension);
    }
    for (XSParticle p : children) {
      XSElementDecl element = p.getTerm().asElementDecl(); //xs:element inside complex type
      getLog().info(element.getType().getName() + " " + element.getName());
      JavaType fieldType = getFieldType(p, element.getType(), targetNamespace);
      JavaField field = javaClass.addField().withName(element.getName())
              .withModifier("protected")
              .withType(fieldType);
      if (element.getIdentityConstraints().size() > 0) {
        field.addAnnotation(Column.class).withValue("unique", "true");
      }
      javaClass.addGetMethod(field);
      javaClass.addSetMethod(field);
      if (fieldType.isEntity) {
        if (fieldType.collectionName != null) {
          field.withAnnotations(OneToMany.class);
        } else {
          field.withAnnotations(ManyToOne.class);
        }
      } else {
        XSRestrictionSimpleType restriction = element.getType().asSimpleType().asRestriction();
        XSFacet minLength = restriction.getDeclaredFacet(XSFacet.FACET_MINLENGTH);
        XSFacet maxLength = restriction.getDeclaredFacet(XSFacet.FACET_MAXLENGTH);
        if (minLength != null || maxLength != null) {
          JavaAnnotation annotation = field.addAnnotation(Length.class);
          if (minLength != null) {
            annotation.withValue("min", minLength.getValue().value);
          }
          if (minLength != null) {
            annotation.withValue("max", maxLength.getValue().value);
          }
        }
        if (fieldType.isCollection) {
          field.withAnnotations(ElementCollection.class);
        }
      }
      if (!element.isNillable()) {
        field.addAnnotation(NotNull.class);
      }
      if (element.getType().getName().equals("email")) {
        field.addAnnotation(Email.class);
      }
    }
    javaClass.addToStringMethod();
    javaClass.addMetaModel(); // have to be last
    javaClass.write(outputFolder, true);
    System.out.println(javaClass.print());

  }

  private XSParticle[] combineArray(XSParticle[] base, XSParticle[] extension) {
    XSParticle[] result = new XSParticle[base.length + extension.length];
    int i = 0;
    for (XSParticle p : base) {
      result[i++] = p;
    }
    for (XSParticle p : extension) {
      result[i++] = p;
    }
    return result;
  }

  static private Map<String, Class> typesMap;
  static private Map<String, String> enumTypes;

  static {
    typesMap = new HashMap<String, Class>();
    typesMap.put("string", String.class);
    typesMap.put("date", Date.class);
    typesMap.put("datetime", Date.class);
    enumTypes = new HashMap<String, String>();
  }

  private JavaType getFieldType(XSParticle p, XSType type, String targetNamespace) {
    JavaType javaType = new JavaType(type.getName());
    if (type.isComplexType()) {
      javaType.isEntity = type.isComplexType();
      javaType.elementName = type.getName();
      javaType.packageName = targetNamespace + ".stub";
      javaType.name = type.getName();
    } else if (enumTypes.containsKey(type.getName())) {
      javaType.elementName = type.getName();
      javaType.packageName = targetNamespace + ".client";
      javaType.name = type.getName();
    } else {
      Class _clazz = typesMap.get(type.getName());
      if (_clazz == null) {
        _clazz = typesMap.get(type.getBaseType().getName());
      }
      javaType.elementName = _clazz != null ? _clazz.getSimpleName() : type.getName();
      javaType.packageName = _clazz.getPackage().getName();
      javaType.name = javaType.elementName;
    }
    if (p.getMaxOccurs().intValue() == -1) {
      javaType.isCollection = true;
      javaType.collectionName = "java.util.List";
      javaType.name = new StringBuilder("List<").append(javaType.elementName).append(">").toString();
    }
    return javaType;
  }

  private void generateEnum(Map.Entry<String, XSSimpleType> entry, String targetNamespace) {
    // generate enums
    XSRestrictionSimpleType restriction = entry.getValue().asRestriction();
    String name = restriction.getName();
    getLog().info("Typelist found...\t" + name);
    List<XSFacet> codes = restriction.getDeclaredFacets(XSFacet.FACET_ENUMERATION);
    if (!codes.isEmpty()) {
      JavaClass enumClass = new JavaClass().withName(targetNamespace + ".client", name).asEnum();
      for (XSFacet code : codes) {
        enumClass.addCode(code.getValue().value);
      }
      enumClass.write(outputFolder, true);
      enumTypes.put(name, name);
    }
  }

  public static void main(String[] args) throws MojoExecutionException {
    EntityGenerator codeGenerator = new EntityGenerator();
    codeGenerator.inputFolder = "src/domain";
    codeGenerator.outputFolder = "generic/src/main/java";
    codeGenerator.execute();
  }
}
